package com.msb.mall.product.service.impl;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.baomidou.mybatisplus.extension.service.IService;
import com.msb.mall.product.service.CategoryBrandRelationService;
import com.msb.mall.product.vo.Catelog2VO;
import org.apache.commons.lang.StringUtils;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.Caching;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.msb.common.utils.PageUtils;
import com.msb.common.utils.Query;

import com.msb.mall.product.dao.CategoryDao;
import com.msb.mall.product.entity.CategoryEntity;
import com.msb.mall.product.service.CategoryService;
import org.springframework.transaction.annotation.Transactional;


@Service("categoryService")
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService {


    @Autowired
    CategoryBrandRelationService categoryBrandRelationService;

    @Autowired
    CategoryService categoryService;

    @Autowired
    StringRedisTemplate stringRedisTemplate;

    @Autowired
    RedissonClient redissonClient;


    @Override
    public PageUtils queryPage(Map<String, Object> params) {
        IPage<CategoryEntity> page = this.page(
                new Query<CategoryEntity>().getPage(params),
                new QueryWrapper<CategoryEntity>()
        );

        return new PageUtils(page);
    }

    /**
     * 查询所有的类别数据，然后将数据封装为树形结构，便于前端使用
     *
     * @param params
     * @return
     */
    @Override
    public List<CategoryEntity> queryPageWithTree(Map<String, Object> params) {
        // 1.查询所有的商品分类信息
        List<CategoryEntity> categoryEntities = baseMapper.selectList(null);
        // 2.将商品分类信息拆解为树形结构【父子关系】
        // 第一步遍历出所有的大类  parent_cid = 0
        List<CategoryEntity> list = categoryEntities.stream().filter(categoryEntity -> categoryEntity.getParentCid() == 0)
                .map(categoryEntity -> {
                    // 第二步根据大类找到对应的所有的小类 递归的方式实现
                    categoryEntity.setChildrens(getCategoryChildrens(categoryEntity,categoryEntities));
                    return categoryEntity;
                }).sorted((entity1, entity2) -> {
                    //先大类之间进行比较，然后再进行大类中小类之间的比较
                    return (entity1.getSort() == null ? 0 : entity1.getSort()) - (entity2.getSort() == null ? 0 : entity2.getSort());
                    //收集起来并转为list
                }).collect(Collectors.toList());


        return list;

    }


    /**
     *  查找该大类下的所有的小类  递归查找
     * @param categoryEntity 某个大类
     * @param categoryEntities 所有的类别数据
     * @return
     */
    private List<CategoryEntity> getCategoryChildrens(CategoryEntity categoryEntity
            , List<CategoryEntity> categoryEntities) {
        List<CategoryEntity> collect = categoryEntities.stream().filter(entity -> {
            // 根据大类找到他的直属的小类
            return  entity.getParentCid().equals(categoryEntity.getCatId()) ;
        }).map(entity -> {
            // 根据这个小类递归找到对应的小小类
            entity.setChildrens(getCategoryChildrens(entity, categoryEntities));
            return entity;
        }).sorted((entity1, entity2) -> {
            return (entity1.getSort() == null ? 0 : entity1.getSort()) - (entity2.getSort() == null ? 0 : entity2.getSort());
        }).collect(Collectors.toList());
        return collect;
    }



    /**
     * 逻辑批量删除操作
     * @param ids
     */
    @Override
    public void removeCategoryByIds(List<Long> ids) {


        // 批量逻辑删除操作
        baseMapper.deleteBatchIds(ids);

    }

    /**
     * 获取类别的路径
     * @param catelogId
     * @return
     */
    @Override
    public Long[] findCatelogPath(Long catelogId) {

        List<Long> paths = new ArrayList<>();

        //利用递归获取catelogId的父级节点，假设传过去的子节点为225，最后获取到的集合为{225，25，2}
       List<Long> parentPath =findParentPath(catelogId,paths);
        //反转指定列表中元素的顺序225，25，2反过来2，25，225
        Collections.reverse(parentPath);
        //把集合中的元素按顺序放到数组中，并返回一个数组
        return parentPath.toArray(new Long[parentPath.size()]);
    }

    private List<Long> findParentPath(Long catelogId, List<Long> paths) {


         CategoryEntity categoryEntity = this.getById(catelogId);
        if(catelogId !=0){
            paths.add(catelogId);

            findParentPath(categoryEntity.getParentCid(),paths);
        }

        return paths;
    }


    /**
     * @CacheEvict：在更新数据的时候同步删除缓存中的数据
     * @CacheEvict(value = "catagory",allEntries = true) 表示删除catagory分区下的所有的缓存数据
     * 三级分类表更新了，对应的品牌三级分类关联表也要相应的更新
     * @param category
     */
    //这里有两个对数据库的操作，所有要加事务管理，也要是失败了可以回滚
    @Transactional
    //@CacheEvict(value = "category",key = "'getLevelCategory'")
    @Caching(evict = {@CacheEvict(value = "category",key = "'getLevelCategory'")
                      ,@CacheEvict(value = "category",key = "'getCateLog2JSON'")})
    @Override
    public void updateDatali(CategoryEntity category) {

        this.updateById(category);

        if (!StringUtils.isEmpty(category.getName())){

            categoryBrandRelationService.updateCategoryName(category.getCatId(), category.getName());

        }
    }


    /**
     *TODO 查询出所有的商品大类(一级分类)
     *    在注解中我们可以指定对应的缓存的名称，起到一个分区的作用，一般按照业务来区分
     *    @Cacheable({"catagory","product"}) 代表当前的方法的返回结果是需要缓存的，
     *                                       调用该方法的时候，如果缓存中有数据，那么该方法就不会执行，
     *                                       如果缓存中没有数据，那么就执行该方法并且把查询的结果缓存起来
     *    缓存处理
     *       1.存储在Redis中的缓存数据的Key是默认生成的：缓存名称::SimpleKey[]
     *       2.默认缓存的数据的过期时间是-1永久
     *       3.缓存的数据，默认使用的是jdk的序列化机制
     *    改进：
     *       1.生成的缓存数据我们需要指定自定义的key： key属性来指定，可以直接字符串定义也可以通过SPEL表达式处理：#root.method.name
     *       2.指定缓存数据的存活时间: spring.cache.redis.time-to-live 指定过期时间
     *       3.把缓存的数据保存为JSON数据
     *   SpringCache的原理
     *     CacheAutoConfiguration--》根据指定的spring.cache.type=reids会导入 RedisCacheAutoConfiguration
     * @return
     */

    @Cacheable(value = "category",key = "#root.method.name" ,sync = true)
    @Override
    public List<CategoryEntity> getLevelCategory() {
        System.out.println("查询了数据库操作....");
        long timeMillis = System.currentTimeMillis();
        List<CategoryEntity> list=baseMapper.queryLeveCategory();
        System.out.println("消耗了时间...."+(System.currentTimeMillis()-timeMillis));
        return list;
    }

    /**
     * todo 利用redis做为分布式缓存,来储存二级，三级分类的信息,并查找
     *     下面用的是springcache做缓存一致性
     * @return
     */
    @Cacheable(value = "category",key = "#root.method.name")
    @Override
    public Map<String, List<Catelog2VO>> getCateLog2JSON(){


        System.out.println("-----------》查询数据库操作");

        //获取所有分类的数据
        List<CategoryEntity> categoryEntityList = baseMapper.selectList(new QueryWrapper<CategoryEntity>());

        // 获取所有的一级分类的数据
        List<CategoryEntity> category = queryByParentCid(categoryEntityList, 0L);
        // 把一级分类的数据转换为Map容器 key就是一级分类的编号， value就是一级分类对应的二级分类的数据
        Map<String, List<Catelog2VO>> map = category.stream().collect(Collectors.toMap(key -> key.getCatId().toString()
                , value -> {
                    // 根据一级分类的编号，查询出对应的二级分类的数据
                    List<CategoryEntity> l2Catalogs
                            = queryByParentCid(categoryEntityList, value.getCatId());

                    List<Catelog2VO> Catalog2VOs = null;

                    if (l2Catalogs != null) {

                        Catalog2VOs = l2Catalogs.stream().map(l2 -> {

                            // 需要把查询出来的二级分类的数据填充到对应的Catelog2VO中
                            Catelog2VO catelog2VO = new Catelog2VO(l2.getParentCid(), null, l2.getCatId(), l2.getName());

                            // 根据二级分类的数据找到对应的三级分类的信息
                            List<CategoryEntity> l3Catalogs
                                    = queryByParentCid(categoryEntityList, l2.getCatId());
                            if (l3Catalogs != null) {
                                // 获取到的二级分类对应的三级分类的数据
                                List<Catelog2VO.Catelog3VO> Catalog3VOs = l3Catalogs.stream().map(l3 -> {

                                    //把三级分类储存到Catelog3VO
                                    Catelog2VO.Catelog3VO catelog3VO = new Catelog2VO.Catelog3VO(l3.getCatId(), l3.getParentCid(), l3.getName());

                                    return catelog3VO;
                                }).collect(Collectors.toList());

                                // 三级分类关联二级分类
                                catelog2VO.setCatalog3List(Catalog3VOs);
                            }
                            return catelog2VO;
                        }).collect(Collectors.toList());

                    }
                    return Catalog2VOs;
                }
        ));

        return map;

    }



    /*//本地缓存
    private Map<String, Map<String, List<Catelog2VO>>> cache = new HashMap<>();*/

    /**
     * 利用redis做为分布式缓存,来储存二级，三级分类的信息
     * @return
     */
   // @Override
    public Map<String, List<Catelog2VO>> getCateLog2JSONredis(){

        String key="CatelogJSON";
        // 从Redis中获取分类的信息
        String catelogJSON = stringRedisTemplate.opsForValue().get(key);

        if (StringUtils.isEmpty(catelogJSON)) {
            System.out.println("缓存没命中");
            // 缓存中没有数据，需要从数据库中查询
             Map<String, List<Catelog2VO>> catelog2JSONForDb =getCatelog2JSONDbWithRedisson();

            if (catelog2JSONForDb == null) {
                // TODO 那就说明数据库中也不存在  防止缓存穿透
                stringRedisTemplate.opsForValue().set(key, "1", 10L, TimeUnit.MINUTES);
            } else {
                // 从数据库中查询到的数据，我们需要给缓存中也存储一份
                //TODO 防止缓存雪崩
                String json = JSON.toJSONString(catelog2JSONForDb);//todo 转成json字符串
                stringRedisTemplate.opsForValue().set(key,json,10L,TimeUnit.MINUTES);
            }

            return catelog2JSONForDb;
        }
        // 表示缓存命中了数据，那么从缓存中获取信息，然后返回
        System.out.println("缓存命中了");
         Map<String, List<Catelog2VO>> stringListMap = JSON.parseObject(catelogJSON, new TypeReference<Map<String, List<Catelog2VO>>>() {
        });//todo json字符串转成实体对象，并转成指定类型

        return stringListMap;
    }

    /**
     * 查询出所有的二级和三级分类的数据
     * 并封装为Map<String, Catalog2VO>对象
     * todo 下面用的是redisson分布式锁
     * @return
     */
    public Map<String, List<Catelog2VO>> getCatelog2JSONDbWithRedisson(){

        String keys="CatelogJSON";

        // 获取分布式锁对象  加锁的时候，这个锁的名称一定要注意
        // 商品信息 product-lock  product-1001-lock product-1002-lock
        RLock lock = redissonClient.getLock("categoryJSON-lock");
        Map<String, List<Catelog2VO>> dataForDB =null;

        try {
            //加锁成功
            lock.lock();

           dataForDB = getDataForDB(keys);
        }finally {
            //释放锁
            lock.unlock();
        }

        return dataForDB;


    }

    /**
     * 查询出所有的二级和三级分类的数据
     * 并封装为Map<String, Catalog2VO>对象
     * todo 下面用的是redis分布式锁
     * @return
     */

    public Map<String, List<Catelog2VO>> getCatelog2JSONDbWithRedisLock() {


       /* //判断缓存中是否包含需要的key
        if (cache.containsKey("getCateLoge2JSON")) {
            return cache.get("getCateLoge2JSON");
        }*/

        String keys="CatelogJSON";
        //获取uuid
        String uuid = UUID.randomUUID().toString();
        // 加锁 在执行插入操作的同时设置了过期时间
        Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", uuid,30,TimeUnit.SECONDS);
        //加锁
        if (lock) {
            Map<String, List<Catelog2VO>> dataForDB =null;
            try {
                //查询数据库
               dataForDB = getDataForDB(keys);

            }finally {
                String srcipts = "if redis.call('get',KEYS[1]) == ARGV[1]  then return redis.call('del',KEYS[1]) else  return 0 end ";
                // 通过Redis的lua脚本实现 查询和删除操作的原子性
                stringRedisTemplate.execute(new DefaultRedisScript<Integer>(srcipts,Integer.class)
                        ,Arrays.asList("lock"),uuid);
            }

            return dataForDB;
        } else {

                // 加锁失败
                // 休眠
                //Thread.sleep(100);//让没有抢到锁的线程阻塞100毫秒
                //重试
             return getCatelog2JSONDbWithRedisLock();
        }
    }

    /**
     * 从数据库中查询操作
     * @param keys
     * @return
     */
    private Map<String, List<Catelog2VO>> getDataForDB(String keys) {


            // 先去缓存中查询有没有数据，如果有就返回，否则查询数据库
            // 从Redis中获取分类的信息
            String s = stringRedisTemplate.opsForValue().get(keys);

            if (!StringUtils.isEmpty(s)) {
                // 说明缓存命中
                // 表示缓存命中了数据，那么从缓存中获取信息，然后返回
                Map<String, List<Catelog2VO>> stringListMap = JSON.parseObject(s, new TypeReference<Map<String, List<Catelog2VO>>>() {
                });

                return stringListMap;
            }

            System.out.println("-----------》查询数据库操作");

            //获取所有分类的数据
            List<CategoryEntity> categoryEntityList = baseMapper.selectList(new QueryWrapper<CategoryEntity>());

            // 获取所有的一级分类的数据
            List<CategoryEntity> category = queryByParentCid(categoryEntityList, 0L);
            // 把一级分类的数据转换为Map容器 key就是一级分类的编号， value就是一级分类对应的二级分类的数据
            Map<String, List<Catelog2VO>> map = category.stream().collect(Collectors.toMap(key -> key.getCatId().toString()
                    , value -> {
                        // 根据一级分类的编号，查询出对应的二级分类的数据
                        List<CategoryEntity> l2Catalogs
                                = queryByParentCid(categoryEntityList, value.getCatId());

                        List<Catelog2VO> Catalog2VOs = null;

                        if (l2Catalogs != null) {

                            Catalog2VOs = l2Catalogs.stream().map(l2 -> {

                                // 需要把查询出来的二级分类的数据填充到对应的Catelog2VO中
                                Catelog2VO catelog2VO = new Catelog2VO(l2.getParentCid(), null, l2.getCatId(), l2.getName());

                                // 根据二级分类的数据找到对应的三级分类的信息
                                List<CategoryEntity> l3Catalogs
                                        = queryByParentCid(categoryEntityList, l2.getCatId());
                                if (l3Catalogs != null) {
                                    // 获取到的二级分类对应的三级分类的数据
                                    List<Catelog2VO.Catelog3VO> Catalog3VOs = l3Catalogs.stream().map(l3 -> {

                                        //把三级分类储存到Catelog3VO
                                        Catelog2VO.Catelog3VO catelog3VO = new Catelog2VO.Catelog3VO(l3.getCatId(), l3.getParentCid(), l3.getName());

                                        return catelog3VO;
                                    }).collect(Collectors.toList());

                                    // 三级分类关联二级分类
                                    catelog2VO.setCatalog3List(Catalog3VOs);
                                }
                                return catelog2VO;
                            }).collect(Collectors.toList());

                        }
                        return Catalog2VOs;
                    }
            ));

        /*//设置缓存中的数据
        cache.put("getCateLoge2JSON", map);*/

            if (map == null) {

                // 那就说明数据库中也不存在  防止缓存穿透
                stringRedisTemplate.opsForValue().set(keys, "2", 10L, TimeUnit.MINUTES);

            } else {

                // 从数据库中查询到的数据，我们需要给缓存中也存储一份
                // 防止缓存雪崩
                String stringJson = JSON.toJSONString(map);
                //上面代码也判断了一次redis缓存中是否有数据
                stringRedisTemplate.opsForValue().set(keys, stringJson);

            }


            return map;

    }


    /**
     * 跟据父编号获取对应的子菜单信息，达到减少数据库操作的次数的目的，从而提升服务的性能。
     * @param list 所有分类的信息
     * @param parentcid 父编号
     * @return
     */
    private List<CategoryEntity> queryByParentCid(List<CategoryEntity> list ,Long parentcid){

        List<CategoryEntity> list1 = list.stream().filter(item -> {
            //跟据父编号获取对应的子分类信息,不是就过滤掉
            return item.getParentCid().equals(parentcid);

        }).collect(Collectors.toList());

        return list1;

    }
}